|
Writing custom DB engines
FastReport allows building reports not only on the basis of data defined in the application. You can define your own data sources (connections to DB, queries) right in the report as well. FastReport is supplied with engines for ADO, BDE, IBX. You can create your own engine, and then connect it to FastReport. ![]() The picture below shows the hierarchy of classes intended for creating DB engines. The components of a new engine are highlighted with green color. As you can see, a standard set of the DB engine’s components includes Database, Table and Query. You can realize all these components or some of them (for example, many DB have no component of the Table type). You can also realize components, which are not included into the standard set (for example, the StoredProc analogue). Let us examine basic classes in detail. TfrxDialogComponent is a basic class for all non-visual components, which can be placed into the FastReport dialogue form. There are no any important properties or methods defined in it. TfrxCustomDataSet is a basic class of DB components derived from TDataSet. The components inherited from this class are clones of “Query,” “Table,” and “StoredProc.” As a matter of fact, the class represents a cover over TDataSet. TfrxCustomDataset = class (TfrxDBDataSet) protected procedure SetMasterFields( const Value: String ); virtual ; public property Fields: TFields readonly; property MasterFields: String ; property Active: Boolean; published property Filtered: Boolean; property Master: TfrxDBDataSet; end; The following properties are defined in the class: - DataSet is a link to the buried object of the “TdataSet” type; - Fields is a link to the DataSet.Fields; - Active - whether a data set is active; - Filter - expression for filtering; - Filtered – whether filtering is active; - Master is a link to master dataset in a master-detail relationship. - MasterFields is a list of fields like field1=field2. Used for master-detail relations. A component of the Table type inherits from the given class. For its realization, it is necessary to define lacking properties; as a rule, they are: “Database,” “IndexName,” and “TableName.” Also you should override SetMaster, SetMasterFields methods to allow master-detail relations. TfrxCustomQuery is a basic class for DB components of the “Query” type. The class is a cover for a Query type component. TfrxCustomQuery = class (TfrxCustomDataset) protected function GetSQL: TStrings; virtual ; abstract ; public published property SQL: TStrings; end; The “SQL” and “Params” properties (which are general for all Query components) are defined in the class. Since different Query components have different realization of parameters (for example, TParams and TParameters), the “Params” property has the “TfrxParams” type and is a cover for the concrete parameters’ type. The following methods are defined in this class: - SetSQL is to set the “SQL” property of the component of the “Query” type; - GetSQL is to get the “SQL” property of the component of the “Query” type; - UpdateParams is to copy parameters’ values into the component of the Query type. If a Query component’s parameters are of the TParams type, copying is performed via the frxParamsToTParams standard procedure. Let us illustrate creation process of the DB engine by the IBX example. The full original text of the engine can be found in the SOURCE\IBX directory. Below are some quotations from the source text with our comments. The IBX components around which we will build a cover are: TIBDatabase, TIBTable, and TIBQuery. Accordingly, our components will be named “TfrxIBXDatabase,” “TfrxIBXTable,” and “TfrxIBXQuery.” “TfrxIBXComponents” is another component we should create; it will be placed into the FastReport component palette when registering the engine (in Delphi environment). As soon as this component is placed into a project, Delphi automatically adds a link to the unit of our engine into the “Uses” list. It is convenient to assign one more task in this component, i.e. to define the “DefaultDatabase” property in it, which refers to the existing connection to DB. By default, all the TfrxIBXTable and TfrxIBXQuery components will refer to this connection. It is necessary to inherit the component from the TfrxDBComponents class: TfrxDBComponents = class (TComponent) public end; The description should be returned by one function only, for example ”IBX Components.” We plan to add new methods to the list in the future in order to provide support of the visual query builders. Realization of the “TfrxIBXComponents” component is as following: type private FOldComponents: TfrxIBXComponents; public destructor Destroy; override ; function GetDescription: String ; override ; published end; var constructor TfrxIBXComponents.Create(AOwner: TComponent); begin FOldComponents := IBXComponents; IBXComponents := Self; end; destructor TfrxIBXComponents.Destroy; begin inherited ; end; functionTfrxIBXComponents.GetDescription: String ; begin end; We define the IBXComponents global variable, which will refer to the TfrxIBXComponents component’s copy. If you place the component into the project several times (though it is senseless), you will nevertheless be able to save the link to the previous component and restore it after deleting the component. A link to the connection to DB, which already exists in the project, can be placed into the “DefaultDatabase” property. The way we will write the TfrxIBXTable, TfrxIBXQuery components allows them using this connection by default (actually, this is what we need the IBXComponents global variable for). The following component is the TfrxIBXDatabase one. It represents a cover over the TIBDatabase. TfrxIBXDatabase = class (TfrxDialogComponent) private FTransaction: TIBTransaction; procedure SetConnected(Value: Boolean); procedure SetDatabaseName( const Value: String ); procedure SetLoginPrompt(Value: Boolean); procedure SetParams(Value: TStrings); function GetConnected: Boolean; function GetDatabaseName: String ; function GetLoginPrompt: Boolean; function GetParams: TStrings; function GetSQLDialect: Integer; procedure SetSQLDialect( const Value: Integer); public destructor Destroy; override ; class function GetDescription: String ; override ; property Database: TIBDatabase read FDatabase; published property DatabaseName: String read GetDatabaseName write SetDatabaseName; property LoginPrompt: Boolean read GetLoginPrompt write SetLoginPrompt default True; property Params: TStrings read GetParams write SetParams; property Connected: Boolean read GetConnected write SetConnected default False; property SQLDialect: Integer read GetSQLDialect write SetSQLDialect; end; constructor TfrxIBXDatabase.Create(AOwner: TComponent); begin { create a component – connection } FDatabase := TIBDatabase.Create( nil ); { create a component - transaction (specificity of the IBX) } FTransaction := TIBTransaction.Create( nil ); FDatabase.DefaultTransaction := FTransaction; { do not forget this string! } Component := FDatabase; { component’s icon – take it from the standard set } FImageIndex := 37; end; destructor TfrxIBXDatabase.Destroy; begin FTransaction.Free; { the connection will be deleted automatically in the parent class } inherited ; end; { component’s description will be displayed next to the icon in the objects toolbar } class function TfrxIBXDatabase.GetDescription: String ; begin end; { redirect component’s properties to the cover’s properties and vice versa } function TfrxIBXDatabase.GetConnected: Boolean; begin end; function TfrxIBXDatabase.GetDatabaseName: String ; begin end; function TfrxIBXDatabase.GetLoginPrompt: Boolean; begin end; function TfrxIBXDatabase.GetParams: TStrings; begin end; procedure TfrxIBXDatabase.SetConnected(Value: Boolean); begin FTransaction.Active := Value; end; procedure TfrxIBXDatabase.SetDatabaseName( const Value: String ); begin end; procedure TfrxIBXDatabase.SetLoginPrompt(Value: Boolean); begin end; procedure TfrxIBXDatabase.SetParams(Value: TStrings); begin end; function TfrxIBXDatabase.GetSQLDialect: Integer; begin end; procedure TfrxIBXDatabase.SetSQLDialect( const Value: Integer); begin end; As you can see, this is not that complicated. We create the FDatabase: “TIBDatabase” object, and then define properties we want the designer to possess. The “Get” and “Set” methods are written for each property. The next class is TfrxIBXTable. It inherits, as it was mentioned above, from the TfrxCustomDataSet standard class. All basic functionality (operating with the list of fields, master-detail, basic properties) is already realized in the basic class. We only need to define properties, which are specific for the given component. TfrxIBXTable = class (TfrxCustomDataset) private FTable: TIBTable; procedure SetIndexName( const Value: String ); function GetIndexName: String ; function GetTableName: String ; procedure SetTableName( const Value: String ); procedure SetDatabase( const Value: TfrxIBXDatabase); protected procedure SetMasterFields( const Value: String ); override ; public class function GetDescription: String; override ; property Table: TIBTable read FTable; published property IndexName: String read GetIndexName write SetIndexName; property TableName: String read GetTableName write SetTableName; end; constructor TfrxIBXTable.Create(AOwner: TComponent); begin FTable := TIBTable.Create( nil ); { assign a link to the DataSet property from the basic class – do not forget this string! } DataSet := FTable; { assign a link to connection to DB by default } SetDatabase( nil ); { after that the basic constructor may be called in} inherited ; { component’s icon; we take it from the standard set } FImageIndex := 38; end; class function TfrxIBXTable.GetDescription: String ; begin end; procedure TfrxIBXTable.SetDatabase( const Value: TfrxIBXDatabase); begin FDatabase := Value; { if a value <> nil, connect a table to the selected component } if Value <> nil then { otherwise, try to connect to DB by default, defined in the TfrxIBXComponents component } else if IBXComponents <> nil then { if there were no TfrxIBXComponents for some reason, reset to nil } else end; function TfrxIBXTable.GetIndexName: String ; begin end; function TfrxIBXTable.GetTableName: String ; begin end; procedure TfrxIBXTable.SetIndexName( const Value: String ); begin end; procedure TfrxIBXTable.SetTableName( const Value: String ); begin end; procedure TfrxIBXTable.SetMaster( const Value: TDataSource); begin end; procedure TfrxIBXTable.SetMasterFields( const Value: String ); begin end; Finally, lets examine the last component, “TfrxIBXQuery”. It inherits from the TfrxCustomQuery basic class, in which the necessary properties are already defined. We only need to define the Database property and override the SetMaster method. TfrxIBXQuery = class (TfrxCustomQuery) private FQuery: TIBQuery; procedure SetDatabase( const Value: TfrxIBXDatabase); protected procedure SetSQL(Value: TStrings); override ; function GetSQL: TStrings; override ; public class function GetDescription: String ; override ; procedure UpdateParams; override ; property Query: TIBQuery read FQuery; published end; constructor TfrxIBXQuery.Create(AOwner: TComponent); begin FQuery := TIBQuery.Create( nil ); { assign a link to it to the DataSet property from the basic class – do not forget this line! } Dataset := FQuery; { assign a link to the connection to DB by default } SetDatabase( nil ); { after that a basic constructor may be called in } inherited ; { component’s icon – take it from the standard set } FImageIndex := 39; end; class function TfrxIBXQuery.GetDescription: String ; begin end; procedure TfrxIBXQuery.SetDatabase( const Value: TfrxIBXDatabase); begin FDatabase := Value; if Value <> nil then else if IBXComponents <> nil then else end; procedure TfrxIBXQuery.SetMaster( const Value: TDataSource); begin end; function TfrxIBXQuery.GetSQL: TStrings; begin end; procedure TfrxIBXQuery.SetSQL(Value: TStrings); begin end; procedure TfrxIBXQuery.UpdateParams; begin { this is performed via the standard procedure } frxParamsToTParams(Self, FQuery.Params); end; Registration of all engine’s components is performed in the “Initialization” section. The category, where all the components are placed, is registered in the first place. var initialization CatBmp.LoadFromResourceName(hInstance, 'frxIBX'); frxObjects.RegisterCategory('IBX', CatBmp, 'IBX Components'); { use indexes of standard pictures 37,38,39 instead of pictures} frxObjects.RegisterObject1(TfrxIBXDataBase, nil , '', 'IBX', 0, 37); frxObjects.RegisterObject1(TfrxIBXTable, nil , '', 'IBX', 0, 38); frxObjects.RegisterObject1(TfrxIBXQuery, nil , '', 'IBX', 0, 39); finalization frxObjects.Unregister(TfrxIBXDataBase); frxObjects.Unregister(TfrxIBXTable); frxObjects.Unregister(TfrxIBXQuery); end. It is quite enough for using the engine in reports. There are two more things left at this stage: to register engine’s classes in the script system in order to make them referable from the script, and to register editors of several properties (for example, TfrxIBXTable.TableName) to make the work with the component more convenient. It is better to store the engine’s registration code in a separate file with the RTTI suffix. See more about registration of classes in the script system in the corresponding chapter. Here is an example of such file: unit frxIBXRTTI; interface {$I frx.inc} implementation uses {$IFDEF Delphi6} , Variants {$ENDIF}; type public destructor Destroy; override ; end; var { TFunctions } constructor TFunctions.Create; begin begin AddClass(TfrxIBXDatabase, 'TfrxComponent'); AddClass(TfrxIBXTable, 'TfrxCustomDataset'); AddClass(TfrxIBXQuery, 'TfrxCustomQuery'); AddedBy := nil ; end; end; destructor TFunctions.Destroy; begin inherited ; end; initialization finalization end. It is recommended to place the code of properties’ editors to a separate file with the Editor suffix as well. In our case, it is necessary to write editors to the TfrxIBXDatabase.DatabaseName, TfrxIBXTable.IndexName, TfrxIBXTable.TableName properties. See more about writing properties’ editors in the corresponding chapter. Below is an example of such file: unit frxIBXEditor; interface {$I frx.inc} implementation uses frxDsgnIntf, frxRes, IBDatabase, IBTable {$IFDEF Delphi6} , Variants {$ENDIF} ; type public function Edit: Boolean; override ; end; TfrxTableNameProperty = class (TfrxStringProperty) public procedure GetValues; override ; end; TfrxIndexNameProperty = class (TfrxStringProperty) public procedure GetValues; override ; end; { TfrxDatabaseNameProperty } function TfrxDatabaseNameProperty.GetAttributes: TfrxPropertyAttributes; begin Result := [paDialog]; end; function TfrxDatabaseNameProperty.Edit: Boolean; var db: TIBDatabase; begin db := TfrxIBXDatabase(Component).Database; { create a standard OpenDialog } with TOpenDialog.Create( nil ) do begin { we are interested in *.gdb files } Filter := frxResources.Get('ftDB') + ' (*.gdb)|*.gdb|' + frxResources.Get('ftAllFiles') + ' (*.*)|*.*'; Result := Execute; if Result then begin db.Connected := False; { if a dialogue is completed successfully, assign a new DB name } db.DatabaseName := FileName; db.Connected := SaveConnected; end; Free; end; end ; { TfrxTableNameProperty } function TfrxTableNameProperty.GetAttributes: TfrxPropertyAttributes; begin Result := [paMultiSelect, paValueList]; end; procedure TfrxTableNameProperty.GetValues; var begin { get a link to the TIBTable component } t := TfrxIBXTable(Component).Table; { fill the list of tables available } if t.Database <> nil then end; { TfrxIndexProperty } function TfrxIndexNameProperty.GetAttributes: TfrxPropertyAttributes; begin Result := [paMultiSelect, paValueList]; end; procedure TfrxIndexNameProperty.GetValues; var begin try with TfrxIBXTable(Component).Table do begin IndexDefs.Update; { fill the list of indexes available } for i := 0 to IndexDefs.Count - 1 do end; except end ; end; initialization frxPropertyEditors.Register(TypeInfo( String ), TfrxIBXTable, 'TableName', TfrxTableNameProperty); frxPropertyEditors.Register(TypeInfo( String ), TfrxIBXTable, 'IndexName', TfrxIndexNameProperty); end. |